# coding=utf-8

import sys
import logging
import traceback
import re
import json
sys.path.append('matchutils')
import matchapi
import python_library.utils as utils
from copy import deepcopy
from python_library.simhash import Simhash
from bind_common import BindConfig
from bind_common import BindCommon
from bind_common import ErrCode
from manage_video_load import ManageVideoLoad
reload(sys)
sys.setdefaultencoding("utf-8")


class Match(object):
  """绑定算法
  """

  def __init__(self, manage_album_load):
    """

    Args:
      manage_album_load: 管理加载到内存中的album
    """
    self._manage_album_load = manage_album_load

  def _score_name(self, album_name_list, video_name_list):
    """计算名称得分

    Args:
      album_name_list: album名称列表。包含 name,name_ot,name_cn,name_cn_ot,name_fr,name_fr_ot 等
      video_name_list: video名称列表。仅包含name

    Returns:
      名称得分
    """

    def score_title(title1, title2):
      """计算最相像名称的得分

      Args:
        title1: album最相像的名称
        title2: video最相像的名称

      Returns:
        最相像名称的得分
      """
      set1 = set(BindCommon.sentence_segment(title1))
      logging.debug("max similar album name cut word=%s" % json.dumps(list(set1), encoding=BindConfig.ENCODING, ensure_ascii=False))
      set2 = set(BindCommon.sentence_segment(title2))
      logging.debug("max similar video name cut word=%s" % json.dumps(list(set2), encoding=BindConfig.ENCODING, ensure_ascii=False))
      shared_set = set1.intersection(set2)
      logging.debug("share=%s" % json.dumps(list(shared_set), encoding=BindConfig.ENCODING, ensure_ascii=False))
      if not shared_set:
        logging.debug("no share,return score name")
        return -100
      result = len(shared_set) * 180 / min(len(set1), len(set2))
      logging.debug("share name score=%d" % result)
      return result

    logging.debug("score name:")
    logging.debug("album name list=%s" % json.dumps(album_name_list, encoding=BindConfig.ENCODING, ensure_ascii=False))
    logging.debug("video name list=%s" % json.dumps(video_name_list, encoding=BindConfig.ENCODING, ensure_ascii=False))
    album_name_set, video_name_set = set(album_name_list), set(video_name_list)
    logging.debug("album name set=%s" % json.dumps(list(album_name_set), encoding=BindConfig.ENCODING, ensure_ascii=False))
    logging.debug("video name set=%s" % json.dumps(list(video_name_set), encoding=BindConfig.ENCODING, ensure_ascii=False))
    if album_name_set.intersection(video_name_set):
      logging.debug("album name set intersection video name set, return name score=300")
      return 300

    similar_album_name, similar_video_name = album_name_set.pop(), video_name_set.pop()
    logging.debug("first,pop album_name=%s from album_name_set.pop video_name=%s from video_name_set" % (similar_album_name, similar_video_name))
    max_len = len(utils.longest_common_substring(similar_album_name, similar_video_name))
    logging.debug("now longest common substring length=%d" % max_len)
    for item1, item2 in [(x, y) for x in album_name_set for y in video_name_set]:
      cur_len = len(utils.longest_common_substring(item1, item2))
      if cur_len > max_len:
        similar_album_name, similar_video_name = item1, item2
        max_len = cur_len
    if not max_len:
      logging.debug("album and video not similar name, return")
      return -100
    logging.debug("max similar album name=%s,max similar video name=%s" % (similar_album_name, similar_video_name))
    return score_title(similar_album_name, similar_video_name)

  def _score_field(self, album, video):
    """计算演员，导演，分类得分

    Args:
      album: album信息
      video: video信息

    Returns:
      sub_score: 本方法计算的字段的总等分
      score_dict: 本方法计算的各字段得分
    """
    sub_score = 0
    score_dict = {}
    weight_map = {"actor_name": [80, -100],
                  "director_name": [60, -20],
                  "category_name": [20, 0]}
    for field, weight in weight_map.items():
      logging.debug("+-+-+-+-+-+-+-+score %s,weight=%s" % (field, json.dumps(weight, encoding=BindConfig.ENCODING, ensure_ascii=False)))
      logging.debug("album %s=%s" % (field, json.dumps(album[field], encoding=BindConfig.ENCODING, ensure_ascii=False)))
      logging.debug("video %s=%s" % (field, json.dumps(video[field], encoding=BindConfig.ENCODING, ensure_ascii=False)))
      if not album[field] or not video[field]:
        score_dict[field] = 0
        logging.debug("video or album has not %s,go on next field" % field)
        continue
      album_field_set, video_field_set = set(album[field]), set(video[field])
      logging.debug("album %s set=%s" % (field, json.dumps(list(album_field_set), encoding=BindConfig.ENCODING, ensure_ascii=False)))
      logging.debug("video %s set=%s" % (field, json.dumps(list(video_field_set), encoding=BindConfig.ENCODING, ensure_ascii=False)))
      smaller_len = min(len(album_field_set), len(video_field_set))
      shared_factors = album_field_set.intersection(video_field_set)
      logging.debug("share set=%s" % json.dumps(list(shared_factors), encoding=BindConfig.ENCODING, ensure_ascii=False))
      if not shared_factors:
        logging.debug("not share set,go on next field")
        score_dict[field] = weight[1]
        sub_score += weight[1]
      else:
        score_dict[field] = len(shared_factors) * weight[0] / smaller_len
        logging.debug("%s has share set,score=%d" % (field, score_dict[field]))
        sub_score = sub_score + score_dict[field]
    return sub_score, score_dict

  def _score(self, album, video):
    """计算得分

    Args:
      album: album信息
      video: video信息

    Returns:
      sub_score: 总等分
      score_dict: 各字段得分
    """
    logging.debug("score description:")
    logging.debug("album description=%s" % album["description"])
    logging.debug("video description=%s" % video["description"])
    hash_album = Simhash(album["description"]) if album["description"] and len(album["description"]) > 10 else None
    hash_video = Simhash(video["description"]) if video["description"] and len(video["description"]) > 10 else None
    distance = hash_album.distance(hash_video) \
      if hash_album and hash_video else 20
    logging.debug("hash distance=%d" % distance)
    if distance <= 3:
      logging.debug("description is very similar, return match score")
      return 600, {"intro": 600}

    score = 0
    scorename = self._score_name(album["name"], video["name"])
    logging.debug("finally,score name=%d" % scorename)
    score += scorename
    field_score, score_dict = self._score_field(album, video)
    logging.debug("finally,score field=%d" % field_score)
    score += field_score
    score_dict["name"] = scorename
    return score, score_dict

  def _filter_by_name(self, album, video_dict):
    """绑定前按名称过滤

    Args:
      album: album信息
      video_dict: video信息

    Returns:
      True--过滤, False--不过滤
    """

    def fetch_digit(title):
      """将 名称中第x季 信息中的x归一为阿拉伯数字。并返回该数字

      Args:
        title: 名称

      Returns:
        第x集中的x,为阿拉伯数字，若没有则返回空字符串
      """
      digit_map = {k: str(v) for k, v in zip(list(u"一二三四五六七八九"), range(1, 10))}
      digit_map.update({k: str(v) for k, v
                        in zip(["I", "II", "III", "IV"], range(1, 5))})
      digit_map.update({k: str(v) for k, v in zip(u"零壹贰叁肆伍陆柒捌玖拾", range(0, 11))})
      title = utils.replace_all(title, digit_map, 1)
      a = re.search(u".*?第(\d+)季|.*?(\d+)$", title)
      if a and a.groups():
        for word in a.groups():
          if word:
            return word
      return ""

    if fetch_digit(album["name"][0]) != fetch_digit(video_dict["name"][0]) and album["screen_year"] != video_dict["screen_year"]:
      return True
    name1 = album["name"][0].strip()
    name2 = video_dict["name"][0].strip()
    if u"花絮" in name2 or u"片花" in name2 or u"预告片" in name2:
      return True
    if u"删减版" in name1 or u"删减版" in name2:
      del_type = [u"未删减版", u"无删减版"]
      type_name1 = name1 if len(name1) <= 4 else name1[-4:]
      type_name2 = name2 if len(name2) <= 4 else name2[-4:]
      if (type_name1 in del_type) != (type_name2 in del_type):
        return True
    if u"语版" in name1 or u"语版" in name2 or u"文版" in name1 or u"文版" in name2:
      type_name1 = name1 if len(name1) <= 3 else name1[-3:]
      type_name2 = name2 if len(name2) <= 3 else name2[-3:]
      if type_name1 != type_name2:
        return True
    return False

  def _get_video_keyword_list_old(self, video):
    """按旧算法分桶逻辑计算video关键词列表

    Args:
      video: video信息

    Returns:
      关键词列表
    """
    keyword_list = []
    keyword_list.append(video["name"][0])
    actor_list = video.get("actor_name", [])
    if actor_list:
      keyword_list.extend(actor_list)
    director_list = video.get("director_name", [])
    if director_list:
      keyword_list.extend(director_list)
    return keyword_list


  def _video_pretreatment(self, video):
    """对待进行匹配计算的video进行预处理
       将各字段归一化为可参与匹配的整齐的字段

    Args:
      video: video信息

    Returns:
      二值逻辑：
      True : video信息非法（如名称中包含 “花絮” 等），不可参与后续匹配计算
      False : 可参与后续匹配计算
    """
    # ================= 影响方法返回值，可能导致返回False begin =================
    # 必要字段存在性检查
    logging.debug("video_name=%s" % video["name"])
    if not video["name"]:
      logging.debug("lack video_name,return False")
      return False

    # 必要字段正确性检查
    # name不能包含 第xx集
    video_name_regular = re.search(u'[^第](第.*?集)', video["name"])
    if video_name_regular and video_name_regular.groups():
      for word in video_name_regular.groups():
        if word:
          logging.debug("video_name contain 第xx集,return False")
          return False
    logging.debug("video_name not fillter by 第xx集")
    # ================= 影响方法返回值，可能导致返回False finish =================

    # ================= 处理video字段 begin =================
    # 记录非必要字段的缺失情况
    nonessential_field_list = ["description", "actor_name", "director_name", "category_name"]
    video["lack_fields"] = []
    for nonessential_field in nonessential_field_list:
      if not video.get(nonessential_field):
        video["lack_fields"].append(nonessential_field)
    logging.debug("lack fields=%s" % json.dumps(video["lack_fields"], encoding=BindConfig.ENCODING, ensure_ascii=False))

    video["original_name"] = video["name"]
    # video与album通用的字段处理
    BindCommon.massage(video)
    logging.debug("after handling common fields video=%s" % json.dumps(video, encoding=BindConfig.ENCODING, ensure_ascii=False))
    # 得到video的keyword_list。如果程序能运行至该行，则keyword_list必不为空（必要字段name存在）
    # video["keyword_list"] = self._get_video_keyword_list_old(video)
    video_item = BindCommon.translate_for_new(video, video.get("video_type"))
    video["keyword_list"] = matchapi.get_keywords(video_item)
    logging.debug("video keyword list=%s" % json.dumps(video["keyword_list"], encoding=BindConfig.ENCODING, ensure_ascii=False))
    # ================= 处理video字段 finish =================
    logging.debug("video_pretreatment return True,match go on")
    return True

  def _match_old(self, video_dict, hit_album_list):
    """使用旧绑定算法进行计算

    Args:
      video_dict: video信息
      hit_album_list: 该video命中的album列表

    Returns:
      返回的信息,可能包含的信息有两个：
        error: ErrCode信息，标志本次请求返回值的类型
        ret: 本次请求具体的返回信息
    """
    scores_list = []
    scores_dict_list = []
    for album in hit_album_list:
      logging.debug("%salbum id=%d" % (BindConfig.LOG_MARK, album.get("id")))
      logging.debug("album=%s" % json.dumps(album, encoding=BindConfig.ENCODING, ensure_ascii=False))
      album_dict = deepcopy(album)
      _score, _dict = self._score(album_dict, video_dict)
      logging.debug("total score=%d" % _score)
      _dict["total"] = _score
      _dict["filter_by_name"] = self._filter_by_name(album_dict, video_dict)
      logging.debug("if filter by name=%d" % _dict["filter_by_name"])
      _dict["album_id"] = album.get("id")
      scores_list.append(_score)
      scores_dict_list.append(_dict)

    if scores_list:
      max_score = max(scores_list)
      max_dict = scores_dict_list[scores_list.index(max_score)]
      logging.debug("max similar album score=%s" % json.dumps(max_dict, encoding=BindConfig.ENCODING, ensure_ascii=False))
      max_album_id =  max_dict["album_id"]
      max_album_name = max_dict["filter_by_name"]
      logging.debug("if filter by name=%d" % max_album_name)
      max_dict.pop("filter_by_name")
      max_dict.pop("album_id")

      if max_score >= BindConfig.THRESHOLD and not max_album_name:
        logging.debug("max score>=THRESHOLD and not filter by name,return match")
        if len(video_dict["lack_fields"]) <= 1:
          logging.debug("lack_fields<=1,return match")
          return ErrCode.MATCHED, {"album_id": max_album_id, "score": max_dict, "video_id": video_dict["id"], "lack_fields": video_dict["lack_fields"]}
        else:
          logging.debug("lack_fields>1,return nothing")
          return ErrCode.NOTHING, {"album_id": max_album_id, "score": max_dict, "video_id": video_dict["id"], "lack_fields": video_dict["lack_fields"]}
      elif max_score <= BindConfig.FLOOR_THRESHOLD:
        logging.debug("max score<=FLOOR_THRESHOLD,return new")
        return ErrCode.NEW_ALBUM, {"score": max_dict, "video_id": video_dict["id"], "lack_fields": video_dict["lack_fields"], "album_id": max_album_id}
      else:
        logging.debug("max score in (FLOOR_THRESHOLD, THRESHOLD) || max score>=THRESHOLD and filter by name,return nothing.")
        return ErrCode.NOTHING, {"score": max_dict, "video_id": video_dict["id"], "lack_fields": video_dict["lack_fields"], "album_id": max_album_id}

  def _match_new(self, video_dict, hit_album_list):
    """使用新绑定算法进行计算

    Args:
      video_dict: video信息
      hit_album_list: 该video命中的album列表

    Returns:
      返回的信息,可能包含的信息有两个：
        error: ErrCode信息，标志本次请求返回值的类型
        ret: 本次请求具体的返回信息
    """
    max_score = -1000.0
    max_album_id = 0
    max_album_dict = {}
    video_item = BindCommon.translate_for_new(video_dict, video_dict.get("video_type"))
    logging.debug("video for new algorithm=%s" % json.dumps(video_item, encoding=BindConfig.ENCODING, ensure_ascii=False))
    for album in hit_album_list:
      logging.debug("%salbum id=%d" % (BindConfig.LOG_MARK, album.get("id")))
      logging.debug("album=%s" % json.dumps(album, encoding=BindConfig.ENCODING, ensure_ascii=False))
      album_dict = deepcopy(album)
      album_item = BindCommon.translate_for_new(album_dict, video_dict.get("video_type"))
      logging.debug("album for new algorithm=%s" % json.dumps(album_item, encoding=BindConfig.ENCODING, ensure_ascii=False))
      score = matchapi.match(video_item, album_item)
      logging.debug("score of new algorithm=%s" % str(score))
      if score > max_score:
        max_score = score
        max_album_id = album_dict.get("id")
        max_album_dict = album_dict
    logging.debug("max score=%s,album_id=%s" % (str(max_score), str(max_album_id)))
    result_dict = {"album_id": max_album_id, "score": max_score, "lack_fields": video_dict.get("lack_fields"), "if_conflict": 0}
    if max_score <= BindConfig.NEW_SCORE[0]:
      logging.debug("max score in (-, %s], create new album" % str(BindConfig.NEW_SCORE[0]))
      return ErrCode.NEW_ALBUM, result_dict
    elif max_score > BindConfig.NEW_SCORE[0] and max_score <= BindConfig.NEW_SCORE[1]:
      logging.debug("max score in (%s, %s], create new album unsure" % (str(BindConfig.NEW_SCORE[0]), str(BindConfig.NEW_SCORE[1])))
      score_old, score_dict_old = self._score(max_album_dict, video_dict)
      logging.debug("score_old=%s,score_dict_old=%s" % (str(score_old), json.dumps(score_dict_old, encoding=BindConfig.ENCODING, ensure_ascii=False)))
      if score_old >= BindConfig.THRESHOLD:
        logging.debug("conflict, do nothing")
        result_dict["if_conflict"] = 1
        return ErrCode.NOTHING, result_dict
      else:
        logging.debug("do not conflict, create")
        return ErrCode.NEW_ALBUM, result_dict
    elif max_score > BindConfig.NEW_SCORE[1] and max_score <= BindConfig.NEW_SCORE[2]:
      logging.debug("max score in (%s, %s], unknow" % (str(BindConfig.NEW_SCORE[1]), str(BindConfig.NEW_SCORE[2])))
      return ErrCode.NOTHING, result_dict
    elif max_score > BindConfig.NEW_SCORE[2] and max_score < BindConfig.NEW_SCORE[3]:
      logging.debug("max score in (%s, %s], match unsure" % (str(BindConfig.NEW_SCORE[2]), str(BindConfig.NEW_SCORE[3])))
      score_old, score_dict_old = self._score(max_album_dict, video_dict)
      logging.debug("score_old=%s,score_dict_old=%s" % (str(score_old), json.dumps(score_dict_old, encoding=BindConfig.ENCODING, ensure_ascii=False)))
      if score_old >= BindConfig.THRESHOLD:
        logging.debug("do not conflict, match")
        return ErrCode.MATCHED, result_dict
      else:
        logging.debug("conflict, do nothing")
        result_dict["if_conflict"] = 1
        return ErrCode.NOTHING, result_dict
    else:
      logging.debug("max score in (%s, +], match" % str(BindConfig.NEW_SCORE[3]))
      return ErrCode.MATCHED, result_dict

  def _match_album(self, video):
    """绑定匹配

    Args:
      video: video信息

    Returns:
      返回的信息,可能包含的信息有两个：
        error: ErrCode信息，标志本次请求返回值的类型
        ret: 本次请求具体的返回信息
    """
    video_dict = deepcopy(video)
    logging.debug("%svideo pretreatment begin" % BindConfig.LOG_MARK)
    video_pretreatment_result = self._video_pretreatment(video_dict)
    logging.debug("%svideo pretreatment finish" % BindConfig.LOG_MARK)
    if not video_pretreatment_result:
      logging.debug("video pretreatment return false, match finish return %d" % ErrCode.NOTHING[0])
      return ErrCode.NOTHING, None
    hit_album_set = set()
    hit_album_list = []
    for keyword in video_dict["keyword_list"]:
      album_dicts = self._manage_album_load.album_set.get(video_dict["video_type"], {}).get(keyword, {})
      logging.debug("keyword=%s hit count=%d" % (keyword, len(album_dicts)))
      for album_id, album in album_dicts.items():
        if album_id not in hit_album_set:
          hit_album_set.add(album_id)
          hit_album_list.append(album)

    logging.debug("total keyword hit album count=%d" % len(hit_album_set))
    if len(hit_album_list) == 0:
      logging.debug("no comparison")
      if len(video_dict["lack_fields"]) <= 2:
        logging.debug("lack_fields<=2,return new")
        return ErrCode.NEW_ALBUM, {"lack_fields": video_dict["lack_fields"]}
      else:
        logging.debug("lack_fields>2,return no comparison")
        return ErrCode.NO_COMPARISON, {"lack_fields": video_dict["lack_fields"]}
    # return self._match_old(video_dict, hit_album_list)
    return self._match_new(video_dict, hit_album_list)

  def match_video(self, video_str):
    """绑定匹配

    Args:
      video_str: video id

    Returns:
      返回的信息,可能包含的信息有两个：
        error: ErrCode信息，标志本次请求返回值的类型
        ret: 本次请求具体的返回信息
    """
    logging.debug("receive match request,video_id=%s" % video_str)
    if not video_str:
      logging.debug("Lack necessary parameter video_id,match finish with warn")
      return ErrCode.WRONG_PARAM, None
    try:
      logging.debug("%sget video data begin" % BindConfig.LOG_MARK)
      video_item = ManageVideoLoad.get_video_from_database(video_str)
      if not video_item:
        logging.debug("find no video,match finish with warn")
        return ErrCode.INVALID_VIDEOID, None
      logging.debug("get video data=%s" % json.dumps(video_item, encoding=BindConfig.ENCODING, ensure_ascii=False))
      logging.debug("%sget video data finish" % BindConfig.LOG_MARK)
      return self._match_album(video_item)
    except:
      logging.exception("Failed to match video, video_id:[%s]" % video_str)
      traceback.print_exc()
      return ErrCode.EXCEPT, None